package cat

import (
	"context"
	"database/sql"
	"encoding/json"
	"github.com/speps/go-hashids/v2"
	"go.uber.org/zap"
	"google.golang.org/protobuf/proto"
	"strconv"
	"strings"
	"sync"
	"time"
	"wzgames/internal/admin/admindao"
	"wzgames/internal/admin/model"
	"wzgames/internal/constant"
	"wzgames/internal/gdao/userdao"
	"wzgames/internal/gloader"
	"wzgames/internal/model/gamep"
	"wzgames/internal/model/mcomm"
	"wzgames/internal/model/user"
	"wzgames/internal/response"
	"wzgames/internal/secure"
	"wzgames/pkg/components/mysql"
	"wzgames/pkg/components/redis"
	"wzgames/pkg/logger"
	"wzgames/pkg/utils/byteutil"
	"wzgames/pkg/utils/dateutil"
	"wzgames/pkg/utils/stringutil"
)

var (
	passport *Passport
	ponce    = sync.Once{}
)

const (
	defaultTimeout   = time.Second * 5
	defaultTimeout10 = time.Second * 10
)

type Passport struct {
	hashids *hashids.HashID

	userDs  *mysql.DS
	adminDs *mysql.DS
	redisC  *redis.RedisClient
}

func Init() error {
	var err error
	if passport == nil {
		ponce.Do(func() {
			passport = &Passport{}
			hd := hashids.NewData() // 生成Token用
			hd.Salt = constant.PassportTicketSalt
			hd.MinLength = 32
			h, e := hashids.NewWithData(hd)
			if e != nil {
				logger.Error("[Cat] init hash ids instance fail", zap.Error(e))
				err = e
			} else {
				passport.hashids = h
			}
			passport.userDs = gloader.CommMgr().GetDBProxy(constant.MysqlOfUser)
			passport.adminDs = gloader.CommMgr().GetDBProxy(constant.MysqlOfAdmin)
			passport.redisC = gloader.CommMgr().RedisPool()
		})
	}
	return err
}

// CheckAccount 检测账户，不存在则创建
func (pp *Passport) CheckAccount(agt *gloader.GAgent, req UBaseInfo) (string, uint64, int16) {
	uh := secure.Md5Hex(byteutil.StringToSliceByte(agt.UniquePlayerKey(req.AgtUserId, req.AgtUsername)))
	key := pp.genUCacheKey(uh, agt.AgtId())
	toCreated := false
	userCache, err := pp.getAccountFromCache(key)
	if err != nil || userCache == nil {
		accDao := userdao.AccountDao{Driver: pp.userDs.Ds()}
		uhash, e1 := accDao.GetUidByUHash(uh, agt.AgtId())
		if e1 != nil {
			if e1 != sql.ErrNoRows {
				logger.Error("[Cat] GetAccountByName Error", zap.Int64("AgtId", req.AgtID), zap.String("User", req.AgtUsername), zap.Error(e1))
				return "", 0, response.ErrInternal
			}
			toCreated = true
		} else {
			userE, e2 := accDao.GetAccountByUid(uhash.Uid)
			if e2 != nil {
				logger.Error("[Cat] GetAccountByUid Error", zap.Int64("AgtId", req.AgtID), zap.String("User", req.AgtUsername), zap.Uint64("Uid", uhash.Uid), zap.Error(e2))
				return "", 0, response.ErrInternal
			}
			userCache, err = pp.setAccountCache(key, userE) // 缓存信息
			if err != nil {
				logger.Error("err == ", zap.String("error", err.Error()))
				return "", 0, response.ErrInternal
			}
		}
	}

	if toCreated {
		// 不存在则创建，并缓存信息
		userE := pp.createAccount(agt, req, uh)
		if userE == nil {
			return "", 0, response.ErrInternal
		}
		userCache, err = pp.setAccountCache(key, userE)
		if err != nil {
			return "", 0, response.ErrInternal
		}
	}

	if userCache.Status != mcomm.Status_Up {
		return "", 0, response.ErrUserLocked
	}

	return uh, userCache.Uid, 0
}

func (pp *Passport) CheckAccountExists(agt *gloader.GAgent, req UBaseInfo) (uint64, int16) {
	uh := secure.Md5Hex(byteutil.StringToSliceByte(agt.UniquePlayerKey(req.AgtUserId, req.AgtUsername)))
	key := pp.genUCacheKey(uh, agt.AgtId()) // 玩家缓存Key
	userCache, err := pp.getAccountFromCache(key)
	if err != nil || userCache == nil {
		accDao := userdao.AccountDao{Driver: pp.userDs.Ds()}
		uHash, e1 := accDao.GetUidByUHash(uh, agt.AgtId())
		if e1 != nil {
			if e1 != sql.ErrNoRows {
				logger.Error("[Cat] GetAccountByName Error on CheckAccountExists", zap.Int64("AgtId", req.AgtID), zap.String("User", req.AgtUsername), zap.Error(e1))
				return 0, response.ErrInternal
			}
			return 0, response.ErrNotFound
		}

		return uHash.Uid, 0
	}

	return userCache.Uid, 0
}

func (pp *Passport) createAccount(agt *gloader.GAgent, req UBaseInfo, uh string) *user.UserEntity {
	tx, err := pp.userDs.Ds().Beginx()
	if err != nil {
		logger.Error("[Cat] createAccount fail on start tx", zap.Int64("AgtId", req.AgtID), zap.String("User", req.AgtUsername), zap.Error(err))
		return nil
	}
	defer tx.Rollback()

	newid := gloader.CommMgr().GenID(1001, 0)
	if newid == 0 {
		logger.Error("[Cat] createAccount fail on gen uid", zap.Int64("AgtId", req.AgtID), zap.String("User", req.AgtUsername))
		return nil
	}

	updated := dateutil.UTC()

	account := &user.UserEntity{
		Uid:     newid,
		Uname:   req.AgtUsername,
		AgtId:   agt.AgtId(),
		Agtuid:  req.AgtUserId,
		Status:  mcomm.Status_Up,
		Uhead:   "",
		Created: updated,
		Updated: updated,
	}

	dao := userdao.AccountDao{Driver: tx}
	err = dao.CreateAccount(account) // create account
	if err != nil {
		logger.Error("[Cat] createAccount fail", zap.Int64("AgtId", req.AgtID), zap.String("User", req.AgtUsername), zap.String("AgtUid", req.AgtUserId), zap.Error(err))
		return nil
	}

	uHashEnt := &user.UserHashEntity{
		UHash: uh,
		Uid:   newid,
		AgtId: req.AgtID,
	}
	err = dao.CreateUHash(uHashEnt)
	if err != nil {
		logger.Error("[Cat] createAccount fail on user name hash", zap.Int64("AgtId", req.AgtID), zap.String("User", req.AgtUsername), zap.String("AgtUid", req.AgtUserId), zap.Error(err))
		return nil
	}

	err = dao.InitUserLatest(newid, req.AgtID, updated)
	if err != nil {
		logger.Error("[Cat] createAccount fail on InitUserLatest", zap.Int64("AgtId", req.AgtID), zap.String("User", req.AgtUsername), zap.String("AgtUid", req.AgtUserId), zap.Error(err))
		return nil
	}

	err = tx.Commit()
	if err == nil {
		return account
	}
	return nil
}

func (pp *Passport) genUCacheKey(uh string, agtId int64) string {
	var bd strings.Builder
	bd.WriteString(gloader.EnvString()) // 附上运行环境
	bd.WriteString(constant.CacheAKey)
	bd.WriteString(strconv.FormatInt(agtId, 10))
	bd.WriteString(constant.CacheUKey)
	bd.WriteString(uh)

	return bd.String()
}

func (pp *Passport) getAccountFromCache(key string) (*user.UserCache, error) {
	ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout)
	r, e := pp.redisC.Get().Get(ctx, key).Bytes()
	cancel()
	if e != nil {
		return nil, e
	}
	acc := &user.UserCache{}
	e = proto.Unmarshal(r, acc)
	if e != nil {
		logger.Error("[Cat] getAccountFromCache fail on unmarshal", zap.String("Key", key), zap.Error(e))
		return nil, e
	}
	return acc, nil
}

func (pp *Passport) setAccountCache(key string, acc *user.UserEntity) (*user.UserCache, error) {
	cache := &user.UserCache{
		Uid:    acc.Uid,
		Uname:  acc.Uname,
		AgtId:  acc.AgtId,
		AgtUid: acc.Agtuid,
		Status: acc.Status,
		Uhead:  acc.Uhead,
	}
	b, err := proto.Marshal(cache)
	if err != nil {
		logger.Error("[Cat] setAccountCache fail on acc marshal", zap.Int64("AgtId", acc.AgtId), zap.String("User", acc.Uname), zap.Error(err))
		return nil, err
	}
	ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout)
	_, err = pp.redisC.Get().Set(ctx, key, b, dateutil.Day*time.Second).Result()
	cancel()
	if err != nil {
		logger.Error("[Cat] setAccountCache fail on setex", zap.Int64("AgtId", acc.AgtId), zap.String("User", acc.Uname), zap.Error(err))
		return nil, err
	}
	return cache, nil
}

func (pp *Passport) genTicketKey(token string) string {
	var bd strings.Builder
	bd.WriteString(gloader.EnvString())
	bd.WriteString(constant.CacheAuthKey)
	bd.WriteString(token)

	return bd.String()
}

func (pp *Passport) uidKey(uid uint64) string {
	var bd strings.Builder
	bd.WriteString(gloader.EnvString())
	bd.WriteString(constant.CacheUidToToken)
	bd.WriteString(strconv.FormatUint(uid, 10))

	return bd.String()
}

// GenTicket B端玩家登录后生成的Token
func (pp *Passport) GenTicket(uh, bToken string, agtId int64, uid uint64) (string, error) {
	token, err := pp.hashids.EncodeInt64([]int64{gloader.EnvHashAdjust(), agtId, int64(uid), dateutil.UnixMilli()})
	if err != nil {
		logger.Error("[Cat] GenTicket fail on hashids", zap.Int64("AgtId", agtId), zap.Uint64("UserID", uid), zap.Error(err))
		return "", err
	}

	uTokenKey := pp.uidKey(uid)
	ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout)
	lastToken, _ := pp.redisC.Get().HGet(ctx, uTokenKey, constant.LastTokenKey).Result()
	cancel()
	if lastToken != "" { // 移除旧的
		ctx, cancel = context.WithTimeout(context.Background(), defaultTimeout)
		pp.redisC.Get().Del(ctx, pp.genTicketKey(lastToken))
		cancel()
	}
	// 设置新的
	key := pp.genTicketKey(token)
	ctx, cancel = context.WithTimeout(context.Background(), defaultTimeout10)
	_, err = pp.redisC.Get().HSet(ctx, key, constant.AgtIdKey, agtId, constant.UhKey, uh, constant.CacheBTokenKey, bToken).Result()
	if err == nil {
		_, err = pp.redisC.Get().HSet(ctx, uTokenKey, constant.LastTokenKey, token).Result()
	}
	cancel()
	// TODO 过期时间
	pp.redisC.Get().Expire(context.Background(), uTokenKey, dateutil.Day*time.Second)
	if err != nil {
		logger.Error("[Cat] GenTicket fail on HSet", zap.Int64("AgtId", agtId), zap.Uint64("UserID", uid), zap.Error(err))
		return "", err
	}
	ctx, cancel = context.WithTimeout(context.Background(), defaultTimeout)
	pp.redisC.Get().Expire(ctx, key, dateutil.Day*time.Second)
	cancel()

	return token, nil
}

// GenGuestTicket 游客Token
func (pp *Passport) GenGuestTicket(agtId int64, uid uint64, uname string) (string, error) {
	token, err := pp.hashids.EncodeInt64([]int64{gloader.EnvHashAdjust(), agtId, int64(uid), dateutil.UnixMilli()})
	if err != nil {
		logger.Error("[Cat] GenGuestTicket fail on hashids", zap.Int64("AgtId", agtId), zap.Uint64("UserID", uid), zap.Error(err))
		return "", err
	}

	key := pp.genTicketKey(token)
	ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout)
	_, err = pp.redisC.Get().HSet(ctx, key, constant.GuestKey, 1, constant.AgtIdKey, agtId, constant.UidKey, uid, constant.UnameKey, uname).Result()
	cancel()
	if err != nil {
		logger.Error("[Cat] GenGuestTicket fail on setex", zap.Int64("AgtId", agtId), zap.Uint64("UserID", uid), zap.Error(err))
		return "", err
	}
	ctx, cancel = context.WithTimeout(context.Background(), defaultTimeout)
	pp.redisC.Get().Expire(ctx, key, dateutil.HalfDay*time.Second)
	cancel()

	return token, nil
}

// CheckTicket 检查ticket
// clientSessToken 客户端sessionToken用来和Token绑定
func (pp *Passport) CheckTicket(token, clientSessToken string) (*user.UserCache, bool, string, int16) {
	cacheKey := pp.genTicketKey(token)
	ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout)
	ret, err := pp.redisC.Get().HGetAll(ctx, cacheKey).Result()
	cancel()
	if err != nil {
		logger.Error("[Cat] CheckTicket fail", zap.String("Token", token), zap.Error(err))
		return nil, false, "", response.ErrInternal
	}
	if ret == nil {
		return nil, false, "", response.ErrUnAuthorize
	}
	if clientSessionToken, ok := ret[constant.CacheClientSessKey]; ok {
		if clientSessionToken != clientSessToken {
			return nil, false, "", response.ErrUnAuthorize
		}
	} else {
		ctx1, cancel1 := context.WithTimeout(context.Background(), defaultTimeout)
		pp.redisC.Get().HSet(ctx1, cacheKey, constant.CacheClientSessKey, clientSessToken)
		cancel1()
	}
	isGuest := false
	if _, ok := ret[constant.GuestKey]; ok {
		isGuest = true
	}
	var agtIdStr string
	if v, ok := ret[constant.AgtIdKey]; ok {
		agtIdStr = v
	}
	agtId, _ := stringutil.ToInt64(agtIdStr, 0)
	var bToken string
	if !isGuest {
		var uh string
		if v, ok := ret[constant.UhKey]; ok {
			uh = v
		}
		if v, ok := ret[constant.CacheBTokenKey]; ok {
			bToken = v
		}
		if agtId <= 0 || uh == "" {
			return nil, isGuest, bToken, response.ErrUnAuthorize
		}
		ucache, e := pp.getAccountFromCache(pp.genUCacheKey(uh, agtId))
		if e != nil {
			return nil, isGuest, bToken, response.ErrUnAuthorize
		}
		return ucache, isGuest, bToken, 0
	} else {
		bToken = ""
		var uidStr string
		var uname string
		if v, ok := ret[constant.UidKey]; ok {
			uidStr = v
		}
		if v, ok := ret[constant.UnameKey]; ok {
			uname = v
		}
		uid, _ := stringutil.ToUint64(uidStr, 0)
		if uid == 0 || uname == "" {
			return nil, isGuest, bToken, response.ErrUnAuthorize
		}

		return &user.UserCache{
			Uid:    uid,
			Uname:  uname,
			AgtId:  agtId,
			Status: mcomm.Status_Up,
		}, isGuest, bToken, 0
	}
}

// GenUser 返回游客的ID
func (pp *Passport) GenUser() uint64 {
	ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout)
	r, err := pp.redisC.Get().IncrBy(ctx, constant.GuestIdCacheKey, 1).Result()
	cancel()
	if err != nil {
		logger.Error("[Cat] gen guest user fail on IncrBy", zap.Error(err))
		return 0
	}
	return uint64(r)
}

// GetGameCurrency 获取币种方案信息
func (pp *Passport) GetGameCurrency(gcId int64) (*gamep.GameCurrency, int16) {
	var bd strings.Builder
	bd.WriteString(gloader.EnvString())
	bd.WriteString(constant.CacheGameCurrKey)
	bd.WriteString(strconv.FormatInt(gcId, 10))
	key := bd.String()
	ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout)
	r, _ := pp.redisC.Get().Get(ctx, key).Result()
	cancel()
	if r == "" {
		dao := admindao.SysDao{Driver: pp.adminDs.Ds()}
		gameCurr, err := dao.GetGameCurrencyById(gcId)
		if err != nil {
			if err != sql.ErrNoRows {
				logger.Error("[Cat] get game currency fail on GetGameCurrencyById", zap.Int64("GcId", gcId), zap.Error(err))
				return nil, response.ErrInternal
			}
		}
		if gameCurr == nil || gameCurr.GcId == 0 {
			return nil, response.ErrNotFound
		}
		gm := &gamep.GameCurrency{
			GcID:    gcId,
			GameID:  gameCurr.GameId,
			CurrID:  gameCurr.CurrId,
			Curr:    gameCurr.Curr,
			Status:  gameCurr.Status,
			VType:   gameCurr.VType,
			Updated: dateutil.UnixSecs(),
		}
		if gameCurr.VType == mcomm.ValueType_Float64Type {
			item := &gamep.GameCurrencyLimit{}
			err = json.Unmarshal(byteutil.StringToSliceByte(gameCurr.Cfg), item)
			if err != nil {
				return nil, response.ErrInternal
			}
			gm.FloatValueLimit = item
		} else {
			itemStr := &gamep.GameCurrencyLimitStr{}
			err = json.Unmarshal(byteutil.StringToSliceByte(gameCurr.Cfg), itemStr)
			if err != nil {
				return nil, response.ErrInternal
			}
			gm.StrValueLimit = itemStr
		}

		b, err1 := proto.Marshal(gm)
		if err1 != nil {
			logger.Error("[Cat] push sys game currency to cache Error on proto.Marshal", zap.Int64("GcId", gcId), zap.Error(err1))
		} else {
			ctx, cancel = context.WithTimeout(context.Background(), defaultTimeout)
			_, err = pp.redisC.Get().SetEx(ctx, bd.String(), b, dateutil.Day*time.Second).Result()
			cancel()
			if err != nil {
				logger.Error("[Cat] push sys game currency to cache Error on cache store", zap.Int64("GcId", gcId), zap.Error(err))
			}
		}
		return gm, 0
	}
	ret := &gamep.GameCurrency{}
	err := proto.Unmarshal(byteutil.StringToSliceByte(r), ret)
	if err != nil {
		logger.Error("[Cat] get game currency fail on proto.Unmarshal", zap.Int64("GcId", gcId), zap.Error(err))
		return nil, response.ErrInternal
	}

	return ret, 0
}

func (pp *Passport) UpdateInstLoginNode(uid uint64, instId, newNodeId int64) int64 {
	uTokenKey := pp.uidKey(uid)
	fieldKey := constant.InstLoginNodeKey + strconv.FormatInt(instId, 10)
	ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout)
	lastLoginNode, _ := pp.redisC.Get().HGet(ctx, uTokenKey, fieldKey).Result()
	pp.redisC.Get().HSet(ctx, uTokenKey, fieldKey, strconv.FormatInt(newNodeId, 10))
	cancel()
	pp.redisC.Get().Expire(context.Background(), uTokenKey, dateutil.Day*time.Second)
	var loginNode int64
	if lastLoginNode != "" {
		loginNode, _ = stringutil.ToInt64(lastLoginNode, 0)
	}
	return loginNode
}

func (pp *Passport) CreateNodeBootHistory(req *mcomm.SysNodeBoot) {
	dao := admindao.SysDao{Driver: pp.adminDs.Ds()}
	ent := &model.SysNodeBootHistory{
		NodeId:   req.NodeId,
		Dateline: req.Dateline,
	}
	err := dao.CreateNodeBootHistory(ent)
	if err != nil {
		logger.Error("[Cat] create node boot history fail on CreateNodeBootHistory", zap.Int64("NodeId", req.NodeId), zap.Int64("Dateline", req.Dateline), zap.Error(err))
	}
}

func (pp *Passport) CreateUserLogin(data *user.UserLoginEntity) {
	accDao := userdao.AccountDao{Driver: pp.userDs.Ds()}
	err := accDao.CreateUserLogin(data)
	if err != nil {
		logger.Error("[Cat] create user login history fail on CreateUserLogin", zap.Uint64("UserId", data.Uid), zap.Error(err))
	}
}

func (pp *Passport) CreateUserGameLogin(data *user.UserGameLoginEntity) {
	accDao := userdao.AccountDao{Driver: pp.userDs.Ds()}
	err := accDao.CreateUserGameLogin(data)
	if err != nil {
		logger.Error("[Cat] create user game login history fail on CreateUserGameLogin", zap.Uint64("UserId", data.Uid), zap.Int64("InstId", data.InstId), zap.Error(err))
		return
	}
	_, _ = accDao.UpdateUserLatest(data.Uid, data.InstId, data.CurrId, data.GameId, data.Ip, data.Dateline)
}
